using System;
using System.IO;
using System.Globalization;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Security.Permissions;
#if !PocketPC || DesignTime
using System.Runtime.Serialization;
using System.ComponentModel;
#endif
using System.Reflection;

namespace GeoFramework
{
	/// <summary>
	/// Represents a two-dimensional rectangular area.
	/// </summary>
	/// <remarks>
	/// 	<para>Instances of this class are guaranteed to be thread-safe because the class is
	///     immutable (it's properties can only be set via constructors).</para>
    /// </remarks>
    #region Attributes
#if !PocketPC || DesignTime
#if Framework30
    [TypeConverter("GeoFramework.Design.GeographicSizeConverter, GeoFramework.Design, Culture=neutral, Version=1.0.0.0, PublicKeyToken=3ed3cdf4fdda3400")]
#elif Framework20
    [TypeConverter("GeoFramework.Design.GeographicSizeConverter, GeoFramework.Design, Culture=neutral, Version=1.0.0.0, PublicKeyToken=3ed3cdf4fdda3400")]
#else
	[TypeConverter(typeof(Design.GeographicSizeConverter))]
#endif   
	[Serializable()]
    [ImmutableObject(true)]
#endif
#if Framework20
    [XmlSchemaProvider("GetXmlSchema")]
#endif
    #endregion
    public struct GeographicSize : IFormattable
#if !PocketPC || (PocketPC && Framework20)
	, IXmlSerializable
#endif 
#if !PocketPC || DesignTime
	, ISerializable
#endif
    {

        #region Private Variables

        private Angle _Width;
		private Angle _Height;

        #endregion

        #region Static Fields

        /// <summary>Represents a size with no value.</summary>
		public static readonly GeographicSize Empty = new GeographicSize(Angle.Empty, Angle.Empty);
		/// <summary>Represents a size with no value.</summary>
		public static readonly GeographicSize Minimum = new GeographicSize(Angle.Minimum, Angle.Minimum);
		/// <summary>Represents the largest possible size on Earth's surface.</summary>
		public static readonly GeographicSize Maximum = new GeographicSize(Angle.Maximum, Angle.Maximum);
        /// <summary>Represents an invalid geographic size.</summary>
        public static readonly GeographicSize Invalid = new GeographicSize(double.NaN, double.NaN);

        #endregion

        #region Constructors

        ///// <summary>
        ///// Creates a new instance with empty values.
        ///// </summary>
        //public GeographicSize()
        //{
        //    _Width = Angle.Empty;
        //    _Height = Angle.Empty;
        //}

        /// <summary>Creates a new instance.</summary>
        public GeographicSize(Angle width, Angle height)
        {
            _Width = width;
            _Height = height;
        }

		/// <summary>Creates a new instance.</summary>
		public GeographicSize(double width, double height)
			: this(new Angle(width), new Angle(height))
		{}

        /// <summary>
        /// Creates a new size by converting the specified coordinate.
        /// </summary>
        /// <param name="point"></param>
		public GeographicSize(PointD point)
		{
            _Width = new Angle(point.X);
            _Height = new Angle(point.Y);
		}

        /// <summary>
        /// Creates a new instance from the specified string.
        /// </summary>
        /// <param name="value"></param>
        public GeographicSize(string value)
            : this(value, CultureInfo.CurrentCulture)
        { }

        /// <summary>
        /// Creates a new instance from the specified string in the specified culture.
        /// </summary>
        /// <param name="value">A <strong>String</strong> describing a width and height in degrees (e.g. "1,3").</param>
        /// <param name="culture">A <strong>CultureInfo</strong> object describing how to parse the string.</param>
        /// <remarks>This method will attempt to split the specified string into two values, then parse each value
        /// as an Angle object.  The string must contain two numbers separated by a comma (or other character depending
        /// on the culture).</remarks>
        public GeographicSize(string value, CultureInfo culture)
        {
            // Split out the values
            string[] Values = value.Split(culture.TextInfo.ListSeparator.ToCharArray());

            // There should only be two of them
            switch (Values.Length)
            {
                case 2:
                    _Width = Angle.Parse(Values[0], culture);
                    _Height = Angle.Parse(Values[1], culture);
                    break;
                default:
                    throw new ArgumentException("A GeographicSize could not be created from a string because the string was not in an identifiable format.  The format should be \"(w,h)\" where \"w\" represents a width in degrees, and \"h\" represents a height in degrees.  The values should be separated by a comma (or other character depending on the current culture).");
            }
        }

#if !PocketPC || DesignTime
        /// <summary>
        /// Creates a new instance by de-serializing previously-serialized information.
        /// </summary>
        /// <param name="info">A <strong>SerializationInfo</strong> object containing serialized data.</param>
        /// <param name="context">A <strong>StreamingContext</strong> object describing the context of serialization.</param>
        /// <remarks>This constructor will de-serialize a previously-serialized object.</remarks>
        private GeographicSize(SerializationInfo info, StreamingContext context)
        {
            _Width = new Angle(info.GetDouble("Width"));
            _Height = new Angle(info.GetDouble("Height"));
        }
#endif

#endregion

        #region Public Properties

        /// <summary>Returns the ratio of the size's width to its height.</summary>
		public double AspectRatio
		{
			get
			{
                return _Width.DecimalDegrees / _Height.DecimalDegrees;
			}
        }

        /// <summary>
        /// Returns the left-to-right size.
        /// </summary>
        public Angle Width
        {
            get
            {
                return _Width;
            }
        }

        /// <summary>Returns the top-to-bottom size.</summary>
        public Angle Height
        {
            get
            {
                return _Height;
            }
        }

        /// <summary>Indicates if the size has zero values.</summary>
        public bool IsEmpty
        {
            get
            {
                return (_Width.IsEmpty && _Height.IsEmpty);
            }
        }

        #endregion

        #region Public members

        /// <summary>Returns whether the current instance has invalid values.</summary>
        public bool IsInvalid
        {
            get
            {
                return (double.IsNaN(_Width) || double.IsNaN(_Height));
            }
        }

        [CLSCompliant(false)]
		public GeographicSize ToAspectRatio(System.Drawing.Size size)
		{
			// Calculate the aspect ratio
			return ToAspectRatio((double)size.Width / (double)size.Height);
		}

		public GeographicSize ToAspectRatio(double aspectRatio)
		{
			double CurrentAspect = AspectRatio;
			// Do the values already match?
			if(CurrentAspect == aspectRatio) return this;
			// Is the new ratio higher or lower?
			if(aspectRatio > CurrentAspect)
			{
				// Inflate the GeographicRectangle to the new height minus the current height
				// TESTS OK
				return new GeographicSize(
                    new Angle(_Width.DecimalDegrees + 
					(aspectRatio * Height.DecimalDegrees - Width.DecimalDegrees)), 
                    _Height);
			}
			else
			{
				// Inflate the GeographicRectangle to the new height minus the current height
				return new GeographicSize(_Width, 
					new Angle(_Height.DecimalDegrees + (Width.DecimalDegrees / aspectRatio - Height.DecimalDegrees)));
			}
        }

        /// <summary>
        /// Compares the value of the current instance to the specified GeographicSize.
        /// </summary>
        /// <param name="value">A <strong>GeographicSize</strong> object to compare against.</param>
        /// <returns>A <strong>Boolean</strong>, <strong>True</strong> if the values of both objects are precisely the same.</returns>
        public bool Equals(GeographicSize value)
        {
            return Width.Equals(value.Width)
                && Height.Equals(value.Height);
        }

        /// <summary>
        /// Compares the value of the current instance to the specified GeographicSize, to the specified number of decimals.
        /// </summary>
        /// <param name="value">A <strong>GeographicSize</strong> object to compare against.</param>
        /// <param name="decimals">An <strong>Integer</strong> describing how many decimals the values are rounded to before comparison.</param>
        /// <returns>A <strong>Boolean</strong>, <strong>True</strong> if the values of both objects are the same out to the number of decimals specified.</returns>
        public bool Equals(GeographicSize value, int decimals)
        {
            return _Width.Equals(value.Width, decimals)
                && _Height.Equals(value.Height, decimals);
        }

        public override bool Equals(object obj)
        {
            return Equals((GeographicSize)obj);
        }

        /// <summary>
        /// Returns a unique code based on the object's value.
        /// </summary>
        /// <returns></returns>
		public override int GetHashCode()
		{
			return _Width.GetHashCode() ^ _Height.GetHashCode();
		}

		/// <summary>Adds the specified size to the current instance.</summary>
		public GeographicSize Add(GeographicSize size)
		{
			return new GeographicSize(_Width.Add(size.Width.DecimalDegrees),
                _Height.Add(size.Height.DecimalDegrees));
		}

		/// <summary>Subtracts the specified size from the current instance.</summary>
		public GeographicSize Subtract(GeographicSize size)
		{
			return new GeographicSize(_Width - size.Width, _Height - size.Height);
		}
	
        /// <summary>
        /// Multiplies the width and height by the specified size.
        /// </summary>
        /// <param name="size">A <strong>GeographicSize</strong> specifying how to much to multiply the width and height.</param>
        /// <returns>A <strong>GeographicSize</strong> representing the product of the current instance with the specified size.<returns>
		public GeographicSize Multiply(GeographicSize size)
		{
			return new GeographicSize(_Width * size.Width, _Height * size.Height);
		}

        /// <summary>
        /// Multiplies the width and height by the specified value.
        /// </summary>
        /// <param name="size">A <strong>Double</strong> specifying how to much to multiply both the width and height.</param>
        /// <returns>A <strong>GeographicSize</strong> representing the product of the current instance with the specified size.<returns>
        public GeographicSize Multiply(double size)
		{
            return new GeographicSize(new Angle(_Width.DecimalDegrees * size), new Angle(_Height.DecimalDegrees * size));
		}

		public GeographicSize Divide(GeographicSize size)
		{
            return new GeographicSize(new Angle(_Width.DecimalDegrees / size.Width.DecimalDegrees), new Angle(_Height.DecimalDegrees / size.Height.DecimalDegrees));
		}

		public GeographicSize Divide(double size)
		{
            return new GeographicSize(new Angle(_Width.DecimalDegrees / size), new Angle(_Height.DecimalDegrees / size));
        }

        #endregion

        #region Static Methods

        /// <summary>
        /// Returns a GeographicSize whose value matches the specified string.
        /// </summary>
        /// <param name="value">A <strong>String</strong> describing a width, followed by a height.</param>
        /// <returns>A <strong>GeographicSize</strong> whose Width and Height properties match the specified string.</returns>
		public static GeographicSize Parse(string value)
		{
			return Parse(value, CultureInfo.CurrentCulture);
		}

        /// <summary>
        /// Returns a GeographicSize whose value matches the specified string.
        /// </summary>
        /// <param name="value">A <strong>String</strong> describing a width, followed by a height.</param>
        /// <returns>A <strong>GeographicSize</strong> whose Width and Height properties match the specified string.</returns>
        /// <param name="culture">A <strong>CultureInfo</strong> object describing how to parse the specified string.</param>
		public static GeographicSize Parse(string value, CultureInfo culture)
		{
			// Split the string into words
			string[] Values = value.Split(culture.TextInfo.ListSeparator.ToCharArray());
			// How many words are there?
			switch(Values.Length)
			{
				case 2:
					// Extract each item
					try
					{
						Angle Width = Angle.Parse(Values[0], culture);
						Angle Height = Angle.Parse(Values[1], culture);
						// And return a new rectangle
						return new GeographicSize(Width, Height);
					}
					catch
					{
						throw;
					}
				default:
					throw new FormatException(Resources.GeographicSize_InvalidFormat);
			}
        }

        #endregion

        #region Explicit / Implicit Conversions

#if !PocketPC
        public static explicit operator System.Drawing.SizeF(GeographicSize value)
		{
			return new System.Drawing.SizeF((float)value.Width, (float)value.Height);
		}
#endif

		[CLSCompliant(false)]
		public static explicit operator System.Drawing.Size(GeographicSize value)
		{
            return new System.Drawing.Size((int)Math.Round(value.Width.DecimalDegrees, 0), (int)Math.Round(value.Height.DecimalDegrees, 0));
		}

		public static explicit operator GeographicSize(string value)
		{
			return GeographicSize.Parse(value, CultureInfo.CurrentCulture);
		}

		public static explicit operator string(GeographicSize value)
		{
			return value.ToString();
        }

        #endregion

        #region IFormattable Members

        public override string ToString()
		{
			return ToString("G", CultureInfo.CurrentCulture);
		}

		public string ToString(string format)
		{
			return ToString(format, CultureInfo.CurrentCulture);
		}

		public string ToString(string format, CultureInfo culture)
		{
            return Width.ToString(format, culture)
                + culture.TextInfo.ListSeparator + " "
                + Height.ToString(format, culture);
        }

		
        string IFormattable.ToString(string format, IFormatProvider formatProvider)
        {
            return this.ToString();
        }

        #endregion

#if !PocketPC || (PocketPC && Framework20)
		#region IXmlSerializable Members

#if Framework20
        public static XmlQualifiedName GetXmlSchema(XmlSchemaSet xs)
        {
            // Load it from a string
            xs.Add(Xml.SchemaSet);

            // And return the type
            return new XmlQualifiedName("GeographicSize", Xml.Namespace);
        }
#endif


        XmlSchema IXmlSerializable.GetSchema()
        {
            return null;
        }

        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            ReadXml(reader);
        }

        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            WriteXml(writer);
        }

        /// <summary>
        /// Populates the current instance by reading XML from the specified reader.
        /// </summary>
        /// <param name="reader"></param>
        public void ReadXml(XmlReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            // Defer to the Latitude and Longitude serializers for other stuff
            XmlSerializer AngleSerializer = new XmlSerializer(typeof(Angle));

            // Now read in the elements
            while (!reader.EOF)
            {
                // Keep reading until we reach an element
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        switch (reader.Name)
                        {
                            case "Width":
                                reader.Read();
                                _Width = (Angle)AngleSerializer.Deserialize(reader);
                                break;
                            case "Height":
                                reader.Read();
                                _Height = (Angle)AngleSerializer.Deserialize(reader);
                                break;
                            default:
                                // Read the next item
                                reader.Read();
                                break;
                        }
                        break;
                    case XmlNodeType.EndElement:
                        if (String.Compare(reader.Name, "GeographicSize", true, CultureInfo.InvariantCulture) == 0)
                        {
                            // Done reading!
                            return;
                        }
                        else
                        {
                            // Keep going
                            reader.Read();
                            continue;
                        }
                    default:
                        // Read the next item
                        reader.Read();
                        break;
                }
            }
        }

        /// <summary>
        /// Writes the values of the current instance to the specified XML writer.
        /// </summary>
        /// <param name="writer"></param>
        public void WriteXml(XmlWriter writer)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");

            // Defer to the Distance serializer for other stuff
            XmlSerializer AngleSerializer = new XmlSerializer(typeof(Angle));

            writer.WriteStartElement("Width");
            AngleSerializer.Serialize(writer, _Width);
            writer.WriteEndElement();

            writer.WriteStartElement("Height");
            AngleSerializer.Serialize(writer, _Height);
            writer.WriteEndElement();
        }

        #endregion
#endif

#if !PocketPC || DesignTime
        #region ISerializable Members

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]  
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Width", _Width.DecimalDegrees);
            info.AddValue("Height", _Height.DecimalDegrees);
        }

        #endregion
#endif

    }
}
